<?php
/**
 * Created by PhpStorm.
 * User: Inhere
 * Date: 2015/2/28
 * Use : ...
 * File: AbstractDbDriver.php
 */

namespace ulue\core\db\base;

use PDO,
    Debug;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerInterface;
use ulue\core\abstracts\AbstractCore;
use ulue\core\db\base\query\AbstractDataQuery;
use ulue\core\db\base\schema\Schema;
use ulue\core\db\base\command\AbstractDatabase;
use ulue\core\db\base\command\AbstractTable;
use ulue\core\db\base\command\AbstractReader;
use ulue\core\db\base\command\AbstractWriter;
use ulue\core\db\helper\DbHelper;
use ulue\core\db\TheDatabaseException;

abstract class AbstractDbDriver extends AbstractCore implements InterfaceDbDriver,LoggerAwareInterface
{

############################## attribute definition ##############################

    /**
     * @var PDO
     */
    protected $connect          = null;     //数据库链接对象

    /**
     * Property table.
     * @var AbstractTable[]
     */
    protected $_tbs = [];

    /**
     * Property databases.
     * @var AbstractDatabase[]
     */
    protected $_dbs = [];

    /*
     * @var null| AbstractQuery
     */
    protected $query            = null;

    /**
     * 读取操作类实例
     * @var AbstractReader
     */
    protected $reader;

    /**
     * 写入操作类实例
     * @var AbstractWriter
     */
    protected $writer;

    /**
     * @var null|LoggerInterface
     */
    protected $logger = null;

    protected $filterFun        = 'htmlspecialchars';

    /**
     * 当前驱动名 e.g. 'mysql' 'mssql'
     * @var string
     */
    protected $name;

    /**
     * 数据库连接配置(完整格式)
     * 有 dsn 时，会忽略 'driver''host''port' 配置项
     * @var array
     */
    protected $options              = [];

    /**
     * 数据库名
     * @var mixed|string
     */
    protected $database = '';

    /**
     * 数据库表前缀
     * @var string
     */
    protected $tblPrefix = '';

    // protected static $tableAliases  = [];       // 表别名设置数组

    /**
     * pdo预准备绑定的参数数组
     * @var array
     */
    protected $params      = [];

    /**
     * 未处理的sql语句
     * @var string
     */
    protected $rawSql    = null;

    /**
     * 当前要执行的sql语句
     * @var string
     */
    protected $sql    = null;

    /**
     * 最近执行的一条sql语句
     * @var string
     */
    protected $lastSql  = null;

    protected $debug = false;

    /**
     * 执行sql语句得到的结果集
     * @var mixed
     */
    protected $resultSet = null;

    protected $cacheExpire    = 0;     //查询缓存时间

    // protected $insertLastId;       //插入的最后一条ID

    /**
     * 受影响的行数
     * @var int
     */
    protected $affectedRows=0;

    /**
     * 最近一条错误记录
     * @var string|null
     */
    protected $lastError = null;

    const OP_RENAME = 'rename'; # 表、字段重命名
    const OP_COPY   = 'copy'; # 表、字段 复制


//////////////////////////////////////////////// initialize ////////////////////////////////////////////////

    /**
     * @param null|object $connect  数据库连接
     * @param array $options  数据库配置
     */
    public function __construct($connect = null, $options=null)
    {

        $this->connect   = $connect;
        $this->options   = $options;

        $this->database  = $this->getOption('database');
        $this->tblPrefix = $this->getOption('tblPrefix');

        $this->init();

        Debug::trace(" $this->name : $this->database 数据库操作模型实例已准备好。".Debug::countTime());
    }

    /**
     * @return void
     */
    protected function init()
    {}

    abstract public function select($database);

    /**
     * 数据库连接、选择
     * @return $this
     */
    abstract public function connect();

    /**
     * 关掉数据库连接、选择
     * @return $this
     */
    public function disconnect()
    {
        $this->connect=null;

        return $this;
    }

    /**
     * 查询参数重置
     * 每次执行curd后必须执行
     * @access protected
     */
    protected function queryReset()
    {
        Debug::trace("i 查询参数重置",[
            '@attribute $lastSql'   => $this->lastSql,
            ]);

        $this->lastSql      = $this->sql;
        $this->sql          = null;
        $this->params       = [];
        // $this->optionInit(); //查询参数初始化
    }

    /**
     * @return mixed
     */
    abstract public function prepare();

    /**
     * 预准备后，执行 sql语句
     * @return mixed
     */
    abstract public function execute();

    /**
     * (不使用预准备)执行SQL语句，返回受影响的行
     * @param string $sql
     * @return mixed
     */
    abstract public function exec($sql='');

    abstract public function query($sql='');

//////////////////////////////////////////////// Reader ////////////////////////////////////////////////

    /**
     * 执行并返回结果
     * $db->setSql('sql ...')->queryOne();
     * @param string $type
     * @return mixed
     */
    public function queryOne($type='assoc')
    {
        $this->execute();

        return $this->readOne($type);
    }

    /**
     * 执行并返回结果
     * $db->setSql('sql ...')->queryAll();
     * @param null $key
     * @param string $type
     * @return array|null
     */
    public function queryAll($key=null, $type='assoc')
    {
        $this->execute();

        return $this->readAll($key, $type);
    }

    /**
     * 得到一行记录
     * @param string $type
     * @return mixed
     */
    public function readOne($type='assoc')
    {
        if (strtolower($type) == 'array') {
            return $this->readArray();
        }

        if (strtolower($type) == 'assoc') {
            return $this->readAssoc();
        }

        return $this->readObject($type);
    }

    /**
     * 得到多行记录
     * @param null $key
     * @param string $type
     * @return array|null
     */
    public function readAll($key=null, $type='assoc')
    {
        if (strtolower($type) == 'array') {
            return $this->readArrayList($key);
        }

        if (strtolower($type) == 'assoc') {
            return $this->readAssocList($key);
        }

        return $this->readObjectList($key,$type);
    }

    /**
     * readFirst 读取第一行的数据
     * @return  mixed
     */
    public function readFirst()
    {
        $row = $this->fetchArray();

        if ($row) {
            $row = $row[0];
        }

        $this->freeResult();

        return $row;
    }

    abstract public function fetchColumn($offset=0);


    /**
     * readColumn
     * 读取指定列的数据
     * @param int $offset
     * @return  mixed
     */
    public function readColumn($offset = 0)
    {
        $array = $this->fetchColumn($offset);

        $this->freeResult();

        return $array;
    }

    /**
     * @return mixed
     */
    abstract public function fetch();


    /**
     * @return  mixed
     */
    abstract public function fetchArray();

    /**
     * @return mixed
     */
    public function readArray()
    {
        $array = $this->fetchArray();

        $this->freeResult();

        return $array;
    }

    /**
     * @param null|string $key 用记录的某一列的值来设置数据列表的索引键，不设置则是数组默认的数字索引
     * e.g. 可用 主键id等值唯一的列来设置对应记录的索引键
     * @return array
     */
    public function readArrayList($key=null)
    {
        $array = array();

        while ($row = $this->fetchArray()) {
            if ($key) {
                $array[$row[$key]] = $row;
            } else {
                $array[] = $row;
            }
        }

        // Free up system resources and return.
        $this->freeResult();

        return $array;
    }

    /**
     * @return  mixed
     */
    abstract public function fetchAssoc();

    /**
     * @return mixed
     */
    public function readAssoc()
    {
        $assoc = $this->fetchAssoc();

        $this->freeResult();

        return $assoc;
    }

    /**
     * @param null|string $key 用记录的某一列的值来设置数据列表的索引键，不设置则是数组默认的数字索引
     * e.g. 可用 主键id等值唯一的列来设置对应记录的索引键
     * @return array
     */
    public function readAssocList($key=null)
    {
        $array = array();

        // Get all of the rows from the result set.
        while ($row = $this->fetchAssoc()) {
            if ($key) {
                $array[$row[$key]] = $row;
            } else {
                $array[] = $row;
            }
        }

        $this->freeResult();

        return $array;
    }

    abstract public function fetchObject($class='\stdClass', array $ctorArgs=[]);

    /**
     * @param string $class
     * @return mixed
     */
    public function readObject($class='\stdClass')
    {
        $object = $this->fetchObject($class);

        $this->freeResult();

        return $object;
    }

    /**
     * @param null|string $key 用记录的某一列的值来设置数据列表的索引键，不设置则是数组默认的数字索引
     * e.g. 可用 主键id等值唯一的列来设置对应记录的索引键
     * @param string $class 用来赋值的类，必须是存在的类
     * @return array
     */
    public function readObjectList($key = null, $class = '\stdClass')
    {
        $array = array();

        // Get all of the rows from the result set as objects of type $class.
        while ($row = $this->fetchObject($class)) {
            if ($key) {
                $array[$row->$key] = $row;
            } else {
                $array[] = $row;
            }
        }

        // Free up system resources and return.
        $this->freeResult();

        return $array;
    }

    /**
     * @return null|array
     */
    abstract public function fetchAll();

    abstract public function freeResult( $cursor = null);

//////////////////////////////////////////////// Transaction ////////////////////////////////////////////////


    //开启事务 true开启false关闭
    abstract public function beginTrans();

    //提供一个事务
    abstract public function commit();

    //回滚事务
    abstract public function rollback();

//////////////////////////////////////////////// database ////////////////////////////////////////////


    /**
     * @param string $name
     * @return AbstractDatabase
     */
    public function getDatabase($name=null)
    {
        $class = sprintf('\ulue\core\db\%s\command\Database',$this->name);
        $name  = $name ?: $this->database;
        $key   = md5($class.$name);

        if (!isset($this->_dbs[$key])) {
            /**
             * @var $database AbstractDatabase
             */
            $this->_dbs[$key] = new $class($name, $this);
        }

        return $this->_dbs[$key];
    }

    public function getDatabaseName()
    {
        return $this->database;
    }

//////////////////////////////////////////////// table ////////////////////////////////////////////

    /**
     * @param string $table
     * @param string $database
     * @return AbstractTable
     */
    public function getTable($table, $database=null)
    {
        return $this->getDatabase($database)->getTable($table);
    }

    /**
     * @param string $table
     * @param string $database
     * @return Schema
     */
    public function getTableSchema($table, $database=null)
    {
        return $this->getTable($table, $database)->getSchema();
    }

    /**
     * @param string $table
     * @param string $database
     * @return bool
     */
    public function tableExists($table, $database=null)
    {
        return $this->getDatabase($database)->tableExists($table);
    }

    /**
     * 表名检查
     * @param $table
     * @throws TheDatabaseException
     */
    public function verifyTable($table)
    {
        Debug::trace('i 表名称检查验证 ',[
            '@param $table'     => $table,
            'db Tables'         => $this->getDatabase()->getTableNames()
            ]);

        if ( !$this->tableExists($table) ) {
            throw new TheDatabaseException($this->database."数据库中不存在[ {$table} ]表!");
        }

        //初次执行表结构、字段缓存
        // $this->checkTblCache();

    }

    /**
     * get query TableName e.g. myDB.myTable
     * @param $table
     */
    public function queryTableName($table)
    {
        return $this->database.'.'.$table;
    }

    public function getTblPrefix()
    {
        return $this->tblPrefix;
    }

    public function getPrefix()
    {
        return $this->tblPrefix;
    }

    /**
     * 解析转义的表名，给表名加上前缀
     * before: "SELECT * FROM [[admin]] a INNER JOIN [[tag]] b ON a.id=b.ie [[uid]]"
     * after : "SELECT * FROM pre_admin a INNER JOIN pre_tag b ON a.id=b.ie pre_uid"
     * @param string $string
     * @return mixed
     */
    public function resolveTableName($string)
    {
        return DbHelper::resolveTableName($string, $this->tblPrefix);
    }

    /**
     * 前缀检查，即检查表名是否是完整表名称 存在前缀 return true
     * @param $tableName
     * @return bool
     */
    public function hasTablePrefix($tableName)
    {
        return DbHelper::hasPrefix($tableName, $this->tblPrefix );
    }

//////////////////////////////////////////////// verify ////////////////////////////////////////////

    /**
     * 字段验证检查
     * @param $column
     * @param string $table
     * @return bool
     */
    public function verifyField($column, $table)
    {
        return $this->verifyColumn($column,$table);
    }

    /**
     * @param $name
     * @param string $table
     * @throws TheDatabaseException
     * @return bool
     */
    public function verifyColumn($name, $table)
    {
        if (is_string($name)) {
            $name = trim($name);

            // 全字段 || 使用了表(别)名 a.id | b.name | c.* @todo 无其他表数据，无法检测
            if ( $name==='*' || strpos($name, '.') ) {
                return true;
            }

            // 以逗号分隔的多个字段会被拆开，组成数组
            if ( strpos($name,',') ) {
                $name = explode(',', $name);

                foreach ($name as $n) {
                     $this->verifyColumn($n, $table);
                }

                return true;
            }

            // 无表(别)名，仅字段使用了别名 id as uid | name as username
            if ( $pos = stripos($name, ' AS ') ) {
                $name = substr($name, 0, $pos);
            }

            $name  = trim($name,'` ');
            $columns = $this->getTable($table)->getColumnNames();

            if ( !in_array($name, $columns)) {
                throw new TheDatabaseException( $table.' 表中不存在[ '.$name.' ]字段！');
            }

            return true;
        }

        if (is_array($name)) {
            Debug::trace("i 字段验证检查",['@param $name'=>$name]);

            foreach ($name as $n) {
                $this->verifyColumn($n, $table);
            }

            return true;
        }

        return false;
    }

//////////////////////////////////////////////// Query ////////////////////////////////////////////

    /**
     * @param AbstractDataQuery $query
     * @return $this
     */
    public function setQuery(AbstractDataQuery $query)
    {
        $this->query       = $query;

        if ((string)$query) {
            $this->sql = (string)$query;
        }

        return $this;
    }

    /**
     * @param bool $new
     * @return AbstractDataQuery
     */
    public function getQuery($new=false)
    {
        if (!$this->query || $new) {
            $class = sprintf('\ulue\core\db\%s\query\Query',$this->name);

            $this->query = new $class($this);
        }

        return $this->query;
    }

    /**
     * get Independent Query 总是获取新的查询实例
     * @return  AbstractDataQuery
     */
    private function getIndependentQuery()
    {
        static $query;

        if (!$query) {
            $query = $this->getQuery(true);
        }

        return $query;
    }

    /**
     * quoteName
     * @param string $text
     * @return  mixed
     */
    public function quoteName($text)
    {
        return $this->getIndependentQuery()->quoteName($text);
    }

    /**
     * qn
     * @param string $text
     * @return  mixed
     */
    public function qn($text)
    {
        return $this->quoteName($text);
    }

    /**
     * quote
     * @param string $text
     * @param bool   $escape
     * @return  string
     */
    public function quote($text, $escape = true)
    {
        return $this->getIndependentQuery()->quote($text, $escape);
    }

    /**
     * q
     * @param string $text
     * @param bool   $escape
     * @return  string
     */
    public function q($text, $escape = true)
    {
        return $this->quote($text, $escape);
    }

    /**
     * escape
     * @param string $text
     * @param bool   $extra
     * @return  string
     */
    public function escape($text, $extra = true)
    {
        return $this->getIndependentQuery()->escape($text, $extra);
    }

//////////////////////////////////////////////// Attribute ////////////////////////////////////////////

    //获得版本
    public function getVersion()
    {
        return '1.02';
    }

    /**
     * Method to get property Name /driver name
     * @return  string
     */
    public function getName()
    {
        return $this->name;
    }

    /**
     * @return int
     */
    abstract public function getInsertId();

    //获得受影响的行数
    public function getAffectedRows()
    {
        return $this->affectedRows;
    }

    public function setDebug($value)
    {
        $this->debug = (bool)$value;

        return $this;
    }

    public function getDebug()
    {
        return $this->debug;
    }

    /**
     * 获取最近的错误信息
     * @access public
     * @return string
     */
    public function getError()
    {
        return $this->lastError;
    }

//////////////////////////////////////////////// Operator ////////////////////////////////////////////

    # 获得链接对象
    public function getConnect()
    {
        return $this->connect;
    }

    public function setConnect(\PDO $connect)
    {
        $this->connect = $connect;

        return $this;
    }

    public function getReader()
    {
        if (!$this->reader) {
            # code...
        }

        return $this->reader;
    }

    public function setReader($value='')
    {
        # code...
    }

    public function getWriter()
    {
        # code...
    }

    public function setWriter($value='')
    {
        # code...
    }

//////////////////////////////////////////////// Sql ////////////////////////////////////////////

    /**
     * createCmd 返回操作类对象]
     * @param  string $sql
     * @return $this
     **/
    public function createCmd($sql='')
    {
        if ($sql) {
            $this->setSql($sql);
        }

        return $this;
    }

    /**
     * @param $string
     * @return AbstractDbDriver
     */
    public function bySql($string)
    {
        return $this->setSql($string);
    }

    /**
     * @param $string
     * @return $this
     */
    public function setSql($string)
    {
        $this->sql = $this->rawSql = $string;

        return $this;
    }

    /**
     * @return string
     */
    public function getSql()
    {
        return $this->sql;
    }

    /**
     * @return string
     */
    public function getLastSql()
    {
        return $this->lastSql;
    }

    /**
     * getRealSql 得到可用于查询的 SQL statement
     * @throws TheDatabaseException
     * @return array|string|void
     */
    protected function getRealSql()
    {
        if (!$this->sql) {
            throw new TheDatabaseException('没有SQL语句可用于执行！请先设置SQL语句。');
        }

        $this->sql       = $this->resolveTableName($this->sql);
        // $this->sql = $this->lastSql  = Data::slashes($sql);

        return $this->sql;
    }

    /**
     * @param array $params
     * @return $this
     */
    public function addParams(array $params)
    {
        if ( $params ) {
            $this->params = array_merge($this->params, $params);
        }

        return $this;
    }

    /**
     * @param array $params
     * @return $this
     */
    public function setParams(array $params)
    {
        if ($params) {
            $this->params = $params;
        }

        return $this;
    }

    /**
     * @return array
     */
    public function getParams()
    {
        return $this->params;
    }

//////////////////////////////////////////////// Other ////////////////////////////////////////////


    /**
     * Method to get property Options
     * @param   string $name
     * @param   mixed $default
     * @return  mixed
     */
    public function getOption($name, $default = '')
    {
        $value = $default;

        if (array_key_exists($name, $this->options)) {
            $value = $this->options[$name];

            $value = $value ?: $default;
        }

        return $value;
    }

    /**
     * Method to set property options
     * @param   string  $name
     * @param   mixed   $value
     * @return  static  Return self to support chaining.
     */
    public function setOption($name, $value)
    {
        $this->options[$name] = $value;

        if ($name=='database' || $name=='tblPrefix') {
            $this->$name  = $value;
        }

        return $this;
    }

    /**
     * Method to get property Options
     * @return  array
     */
    public function getOptions()
    {
        return $this->options;
    }

    /**
     * Method to set property options
     * @param   array $options
     * @param   bool $merge
     * @return  static  Return self to support chaining.
     */
    public function setOptions($options, $merge=true)
    {
        if ($merge) {
            $this->options = array_merge($this->options, $options);

        } else {
            $this->options = $options;
        }

        //
        $this->database  = $this->getOption('database');
        $this->tblPrefix = $this->getOption('tblPrefix');

        return $this;
    }

    /**
     * Logs a message.
     *
     * @param   string  $level    The level for the log. Use constants belonging to Psr\Log\LogLevel.
     * @param   string  $message  The message.
     * @param   array   $context  Additional context.
     * @return  self
     */
    public function log($level, $message, array $context = array())
    {
        if ($this->logger) {
            $this->logger->log($level, $message, $context);
        }

        return $this;
    }

    /**
     * Sets a logger instance on the object
     * @param   LoggerInterface  $logger  A PSR-3 compliant logger.
     * @return  static
     */
    public function setLogger(LoggerInterface $logger)
    {
        $this->logger = $logger;

        return $this;
    }

    /**
     * 将查询SQL放入调试数组
     * @param $sql
     * @return bool
     */
    protected function debug($sql='')
    {
        if ( !APP_DEBUG ) {
            return false;
        }

        if ($sql) {
            Debug::$sqlCount++; //查询次数加1
            Debug::$SqlExeArr[Debug::$sqlCount]['sql']      = $sql; //压入一条发送成功的SQL
            Debug::$SqlExeArr[Debug::$sqlCount]['startTime']= microtime(true); //
        } else {
            Debug::$SqlExeArr[Debug::$sqlCount]['endTime']  = microtime(true); //
        }

        return true;
    }

    public function __get($name)
    {
        if ($value = $this->getOption($name)) {
            return $value;
        }

        return parent::__get($name);
    }

    //关闭连接
    public function close()
    {}

    public function __destruct()
    {
        $this->close();
    }


}// end class AbstractDbDriver